import ipRegex from "ip-regex";
import { parse } from "tldts";
import { settings } from "./settings";
import { isStarts } from "./utilities";
import { callVendorSuggestion } from "./Vendor";
import { getSortedHistories } from "./histories";
import { filteredBox } from "./signals";

const suggest = {
  百度: [
    true,
    "https://www.baidu.com/sugrec?prod=wise&cb=window.baidu.sug&wd=",
  ],
  好搜: [true, "https://sug.so.360.cn/suggest?callback=window.so.sug&word="],
  必应: [
    true,
    "https://api.bing.com/qsonhs.aspx?&type=cb&cb=window.bing.sug&q=",
  ],
  神马: [true, "https://sugs.m.sm.cn/web?t=w&callback=window.sm.sug&q="],
  头条: [false, "https://m.toutiao.com/2/article/search_sug/?keyword="],
  谷歌: [
    true,
    "https://www.google.com/complete/search?client=youtube&hl=zh-CN&q=",
  ],
  知乎: [false, "https://www.zhihu.com/api/v4/search/suggest?q="],
};

let sugWord = "";
let controller = void 0;
let timeoutId = -1;

function dataParser(word, type, data, call) {
  const da = [];
  switch (type) {
    case "百度": {
      if (data.g)
        for (const g of data.g) {
          if (g.type === "sug") da.push(g.q);
        }
      break;
    }
    case "必应": {
      if (data.AS.FullResults >= 1) {
        for (const { Txt: s } of data.AS.Results[0].Suggests) da.push(s);
      }
      break;
    }
    case "好搜": {
      for (const { word: s } of data.result) da.push(s);
      break;
    }
    case "神马": {
      for (const { w: s } of data.r) da.push(s);
      break;
    }
    case "头条": {
      for (const { keyword: s } of data.data) da.push(s);
      break;
    }
    case "谷歌": {
      for (const [s] of data[1]) da.push(s);
      break;
    }
    case "知乎": {
      for (const { query: s } of data.suggest) da.push(s);
      break;
    }
  }
  if (word === sugWord) call(da);
}

function fetchSug(source, text, error) {
  const addr = suggest[source][1];
  const callback = document.createElement("script");
  callback.className = "jsonp";
  callback.src = addr + encodeURIComponent(text);
  callback.addEventListener("error", error);
  document.body.append(callback);
  timeoutId = setTimeout(() => {
    callback.remove();
    error();
  }, settings.value.searchPart.suggestTimeout);
}

const vendorSug = callVendorSuggestion();

export const suggestsOrigins = Object.keys(suggest);

vendorSug?.name && suggestsOrigins.push(vendorSug.name);

function getSuggest(text) {
  return new Promise((resolve) => {
    const from = suggestsOrigins[settings.value.searchPart.suggestFrom];
    // ---
    clearTimeout(timeoutId);
    controller?.abort();
    const scripts = document.querySelectorAll(".jsonp");
    if (scripts) {
      for (const script of scripts) {
        script.remove();
      }
    }
    // ---
    sugWord = text;
    controller = new AbortController();
    globalThis.baidu = { sug: (d) => dataParser(text, "百度", d, resolve) };
    globalThis.bing = { sug: (d) => dataParser(text, "必应", d, resolve) };
    globalThis.so = { sug: (d) => dataParser(text, "好搜", d, resolve) };
    globalThis.sm = { sug: (d) => dataParser(text, "神马", d, resolve) };
    globalThis.google = {
      ac: { h: (d) => dataParser(text, "谷歌", d, resolve) },
    };
    // ---
    if (!settings.value.searchPart.showSuggest || text === "") {
      resolve([]);
    } else if (from === vendorSug?.name) {
      vendorSug.call(text, resolve);
    } else if (suggest[from][0]) {
      fetchSug(from, text, () => resolve([]));
    } else {
      fetch(suggest[from][1] + encodeURIComponent(text), {
        signal: controller.signal,
      })
        .then((resp) => resp.json())
        .then((data) => dataParser(text, from, data, resolve))
        .catch(() => resolve([]));
      timeoutId = setTimeout(
        () => controller?.abort(),
        settings.value.searchPart.suggestTimeout,
      );
    }
  });
}

globalThis.vendorSugSwitch = (restore) => {
  if (restore) {
    vendorSug?.restore();
  }
};

const suffixes = [
  ".exit",
  ".i2p",
  ".loki",
  ".ipfs",
  ".bbs",
  ".chan",
  ".cyb",
  ".dyn",
  ".epic",
  ".geek",
  ".gopher",
  ".indy",
  ".libre",
  ".neo",
  ".null",
  ".o",
  ".oss",
  ".oz",
  ".parody",
  ".pirate",
  ".bazar",
  ".coin",
  ".emc",
  ".lib",
  ".fur",
  ".glue",
  ".x",
  ".wif",
  ".u",
  ".retardio",
  ".crypto",
  ".nft",
  ".wallet",
  ".polygon",
  ".pudgy",
  ".unstoppable",
  ".888",
  ".dao",
  ".blockchain",
  ".go",
  ".zil",
  ".bitcoin",
  ".austin",
  ".raiin",
  ".tball",
  ".dfz",
  ".pog",
  ".smobler",
  ".lfg",
  ".dream",
  ".mumu",
  ".propykeys",
  ".bald",
  ".wrkx",
  ".secret",
  ".ubu",
  ".witg",
  ".metropolis",
  ".chomp",
  ".ethermail",
  ".xmr",
  ".wifi",
  ".moon",
  ".boomer",
  ".clay",
  ".stepn",
  ".npc",
  ".tea",
  ".quantum",
  ".emir",
  ".donut",
  ".caw",
  ".onchain",
  ".podcast",
  ".bay",
  ".doga",
  ".tribe",
  ".miku",
  ".bitscrunch",
  ".privacy",
  ".her",
  ".ltc",
  ".xec",
  ".rad",
  ".kryptic",
  ".kingdom",
  ".pastor",
  ".ministry",
  ".nibi",
  ".ask",
  ".south",
  ".calicoin",
  ".altimist",
  ".hegecoin",
  ".bobi",
  ".grow",
  ".eth",
  ".basenji",
  ".local",
  ".test",
  ".alt",
  ".invalid",
  ".example",
  ".localhost",
  ".إختبار",
  ".آزمایشی",
  ".测试",
  ".測試",
  ".испытание",
  ".परीक्षा",
  ".δοκιμή",
  ".테스트",
  ".טעסט",
  ".テスト",
  ".பரிட்சை",
  ".xn--kgbechtv",
  ".xn--hgbk6aj7f53bba",
  ".xn--0zwm56d",
  ".xn--g6w251d",
  ".xn--80akhbyknj4f",
  ".xn--11b5bs3a9aj6g",
  ".xn--jxalpdlp",
  ".xn--9t4b11yi5a",
  ".xn--deba0ad",
  ".xn--zckzah",
  ".xn--hlcj6aya9esc7a",
  ".ctc",
];

function checkPrivateSuffixes(domain) {
  return suffixes.some((suffix) => domain.toLowerCase().endsWith(suffix));
}

const reNoAscii = /[^\u0021-\u007E]/g;
const reCut = /^(?:[^/]{3,}\/)?[\u0021-\u007E]+/;
const reUrl = /h[^ht]*(?:t[^tp]*){2}p.*:\/\/\S{3,}(?=\s|$)/gi;
const reUrlGroup = /^h[^ht]*(?:t[^tp]*){2}p(.*):\/\/(\S{3,})$/i;
const reDomain = /^([^/]*[^./]\.[^./:]+|localhost)(:\d{1,5})?(?:\/|$)/i;
const reV4 = /^((?:\d{1,3}\.){3}\d{1,3})(:\d{1,5})?(?:\/|$)/;
const reV6Box = /^\[([\w%.:]+?)](:\d{1,5})?(?:\/|$)/;

function checkPort(port) {
  return port === void 0 || Number.parseInt(port.slice(1), 10) <= 65_535;
}

function checkUrl(content) {
  const domainMatch = content.match(reDomain);
  const v4Match = content.match(reV4);
  const v6Match = content.match(reV6Box);
  if (domainMatch) {
    return (
      domainMatch.length === 3 &&
      (domainMatch[1].toLowerCase() === "localhost" ||
        parse(domainMatch[1])?.isIcann ||
        checkPrivateSuffixes(domainMatch[1])) &&
      checkPort(domainMatch[2])
    );
  } else if (v4Match) {
    return (
      v4Match.length === 3 &&
      ipRegex.v4({ exact: true }).test(v4Match[1]) &&
      checkPort(v4Match[2])
    );
  }
  return (
    v6Match?.length === 3 &&
    ipRegex.v6({ exact: true }).test(v6Match[1]) &&
    checkPort(v6Match[2])
  );
}

function getExtractLink(word) {
  return new Promise((resolve) => {
    const result = [];
    if (word.indexOf("://") > 0) {
      const urls = word.match(reUrl);
      if (Array.isArray(urls)) {
        for (const url of urls) {
          const urlMatch = url.match(reUrlGroup);
          if (urlMatch) {
            const secure = urlMatch[1].toLowerCase().includes("s") ? "s" : "";
            const protocol = `http${secure}://`;
            const fullContent = urlMatch[2];
            if (checkUrl(fullContent)) {
              result.push({
                url: protocol + fullContent,
                flag: "完整",
              });
            }
            if (reNoAscii.test(fullContent)) {
              const pureContent = fullContent.replaceAll(reNoAscii, "");
              if (pureContent !== fullContent && checkUrl(pureContent)) {
                result.push({
                  url: protocol + pureContent,
                  flag: "删字",
                });
              }
              const cuttedContent = reCut.exec(fullContent)?.[0];
              if (
                cuttedContent &&
                pureContent !== cuttedContent &&
                checkUrl(cuttedContent)
              ) {
                result.push({
                  url: protocol + cuttedContent,
                  flag: "裁剪",
                });
              }
            }
          }
        }
      }
    } else {
      const parts = [];
      for (const part of word.split(" ")) {
        if (checkUrl(part) && !parts.includes(part)) {
          result.push(
            ...["http", "https"].map((protocol) => ({
              url: `${protocol}://${part}`,
              flag: void 0,
            })),
          );
          parts.push(part);
        }
      }
    }
    resolve(result);
  });
}

export const suggestSourceNames = ["收藏", "链接", "搜索历史", "搜索建议"];

export function boxFilter(boxes, name, word, empty) {
  if (word === "") {
    return empty ? [] : boxes;
  } else {
    return boxes.filter((box) =>
      isStarts(box[name], word, !settings.value.searchPart.caseInsensitive),
    );
  }
}

export function mergeSuggests(input) {
  const hist = getSortedHistories(input);
  const results = [
    // 0
    (max) =>
      Promise.resolve(
        boxFilter(filteredBox.value, "title", input, true)
          .slice(0, max || void 0)
          .map(({ title, url }) => ({
            type: "favo",
            word: title,
            url,
          })),
      ),
    // 1
    async (max) => {
      const data = await getExtractLink(input);
      return data.slice(0, max || void 0).map(({ url, flag }) => ({
        type: "link",
        word: url,
        flag,
      }));
    },
    // 2
    (max) =>
      Promise.resolve(
        hist.slice(0, max || void 0).map((word) => ({
          type: "history",
          word,
        })),
      ),
    // 3
    async (max) => {
      const data = await getSuggest(input);
      return data
        .filter((word) => !hist.includes(word))
        .slice(0, max || void 0)
        .map((word) => ({
          type: "suggest",
          word,
        }));
    },
  ];
  const promises = [];
  for (const index of settings.value.searchPart.suggestSources) {
    if (index.show) {
      promises.push(results[index.id](index.max));
    }
  }
  return Promise.allSettled(promises).then((results) => {
    const data = [];
    for (const result of results) {
      if (result.value) data.push(...result.value);
    }
    return data;
  });
}
